/*
 *  Copyright 2010 Andrew Kroh
 *
 *  Kroh-IO is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Kroh-IO is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
#include "serial_port.h"

#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>

#ifndef HAVE_CFMAKERAW
#include "cfmakeraw.h"
#endif // HAVE_CFMAKERAW

#include "util.h"

int test_serial_port(const char *path)
{
    dp(DEBUG_TRACE, "Entering test_read(%s)\n", path);

    int file_descriptor = -1;
    int rtn = 0;
    
    // Try opening the serial port. If interrupted
    // prematurely (EINTR), then try again:
    do 
    {
        // Open the serial port read/write with no
        // controlling terminal and do not wait for
        // a connection. O_NONBLOCK flag causes subsequent
        // I/O on the device to be non-blocking.
        file_descriptor = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
    } while (file_descriptor < 0 && errno==EINTR);
    
    if (file_descriptor < 0)
    {
        dp(DEBUG_TRACE, "open() of <%s> failed - %s(%d)\n", 
            path, strerror(errno), errno);
        goto cleanup;
    }
    else
    {
        dp(DEBUG_TRACE, "open() successful of <%s>\n", path);
    }
    
    // Note that open() follows POSIX semantics: multiple open() 
    // calls to the same file will succeed unless the TIOCEXCL 
    // ioctl is issued. This will prevent additional opens except 
    // by root-owned processes.
    if (ioctl(file_descriptor, TIOCEXCL) < 0)
    {
        dp(DEBUG_WARN, "ioctl() failed to set TIOCEXCL for <%s> - %s(%d)\n", 
           path, strerror(errno), errno);
        goto cleanup;
    }

    // Get the current options and save them so we 
    // can restore the default settings later:
    struct termios saved_term_attrs;
    if (tcgetattr(file_descriptor, &saved_term_attrs) < 0)
    {
        dp(DEBUG_WARN, "tcgetattr() failed to get port settings "
            "for <%s> - %s(%d)\n", path, strerror(errno), errno);
        goto cleanup;
    }
    
    // cfmakeraw sets the flags stored in the termios structure
    // to a state disabling all input and output processing,
    // giving a raw IO path. Setting VMIN and VTIME to zero
    // causes reads to not block.
    struct termios raw_io_tty;
    memcpy(&raw_io_tty, &saved_term_attrs, sizeof(struct termios));
    cfmakeraw(&raw_io_tty);
    raw_io_tty.c_cc[VMIN] = 0;
    raw_io_tty.c_cc[VTIME] = 0;
    
    // TCSANOW causes changes to attributes immediately:
    if (tcsetattr(file_descriptor, TCSANOW, &raw_io_tty) < 0)
    {
        dp(DEBUG_WARN, "tcsetattr() failed to change attributes "
            "on <%s> - %s(%d)\n", path, strerror(errno), errno);
        goto cleanup;
    }
    
    // Perform the test read:
    char testChar;
    if	(read(file_descriptor, &testChar, 1) < 0)
    {
        // Note: EWOULDBLOCK is alias to EAGAIN on OS X but
        //       EAGAIN seems to be the most common accross
        //       all platforms
        
        // The only allowed error is EAGAIN which means
        // that the operation would block if we were in
        // blocking mode, but we are in O_NONBLOCK mode.
        if (errno != EAGAIN)
        {
            dp(DEBUG_WARN, "read() failed for <%s> - %s(%d)\n",
                path, strerror(errno), errno);
            goto cleanup;
        }
    }

    // Only successful reads will make it here:
    rtn = 1;
    
cleanup:

    // Traditionally it is good practice to reset a serial 
    // port back to the state in which you found it. This 
    // is why the original termios struct was saved.
    if (file_descriptor >= 0 && 
          tcsetattr(file_descriptor, TCSANOW, &saved_term_attrs) < 0)
    {
        dp(DEBUG_WARN, "tcsetattr() failed resetting port tty "
            "attributes <%s> - %s(%d)\n", path, strerror(errno), errno);
    }

    // Try closing the serial port. If interrupted
    // prematurely (EINTR), then try again:
    int close_rtn;
    do 
    {
        close_rtn = close(file_descriptor);
    } while (close_rtn < 0 && errno == EINTR);
    
    dp(DEBUG_TRACE, "Exiting test_read() - returning %d\n", rtn);

    return rtn;
}

int open_port(const char *path)
{
    dp(DEBUG_TRACE, "Entering open_port(%s)\n", path);

    int file_descriptor = -1;
    
    // Try opening the serial port. If interrupted
    // prematurely (EINTR), then try again:
    do 
    {
        // Open the serial port read/write with no
        // controlling terminal and do not wait for
        // a connection. O_NONBLOCK flag causes subsequent
        // I/O on the device to be non-blocking.
        file_descriptor = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
	}  while (file_descriptor < 0 && errno==EINTR);
    
    if (file_descriptor < 0)
    {
        dp(DEBUG_TRACE, "open() of <%s> failed - %s(%d)\n", 
            path, strerror(errno), errno);
    }
    else
    {
        dp(DEBUG_TRACE, "open() successful of <%s>\n", path);
        
        // Note that open() follows POSIX semantics: multiple open() 
        // calls to the same file will succeed unless the TIOCEXCL 
        // ioctl is issued. This will prevent additional opens except 
        // by root-owned processes.
        if (ioctl(file_descriptor, TIOCEXCL) < 0)
        {
            dp(DEBUG_WARN, "ioctl() failed to set TIOCEXCL for <%s> - %s(%d)\n", 
               path, strerror(errno), errno);
            
            // Cleanup by closing the port and 
            close(file_descriptor);
            file_descriptor = -1;
        }
    }
    
    return file_descriptor;
}

int get_baud_rate(int file_descriptor)
{
    dp(DEBUG_TRACE, "Entering get_baud_rate(%d)\n", file_descriptor);

    struct termios term_attrs;
    int rtn = -1;

    if (tcgetattr(file_descriptor, &term_attrs) < 0)
    {
        dp(DEBUG_WARN, "tcgetattr() failed to get port settings "
            "for FD <%d> - %s(%d)\n", file_descriptor, strerror(errno), errno);
        goto error;
    }
    
    speed_t baud = cfgetospeed(&term_attrs);
    
    // Convert the speed_t type to an integer baud rate value:
    int baud_value = -1;
    switch (baud)
    {
        // First do those specified by POSIX.
        case B0: baud_value = 0; break;
        case B50: baud_value = 50; break;
        case B75: baud_value = 75; break;
        case B110: baud_value = 110; break;
        case B134: baud_value = 134; break;
        case B150: baud_value = 150; break;
        case B200: baud_value = 200; break;
        case B300: baud_value = 300; break;
        case B600: baud_value = 600; break;
        case B1200: baud_value = 1200; break;
        case B1800: baud_value = 1800; break;
        case B2400: baud_value = 2400; break;
        case B4800: baud_value = 4800; break;
        case B9600: baud_value = 9600; break;
        case B19200: baud_value = 19200; break;
        case B38400: baud_value = 38400; break;
            
        // Now conditionally handle a bunch of extended rates.
        # ifdef B7200
        case B7200: baud_value = 7200; break;
        # endif
        # ifdef B14400
        case B14400: baud_value = 14400; break;
        # endif
        # ifdef B57600
        case B57600: baud_value = 57600; break;
        # endif
        # ifdef B115200
        case B115200: baud_value = 115200; break;
        # endif
        # ifdef B230400
        case B230400: baud_value = 230400; break;
        # endif
        # ifdef B460800
        case B460800: baud_value = 460800; break;
        # endif
        # ifdef B500000
        case B500000: baud_value = 500000; break;
        # endif
        # ifdef B576000
        case B576000: baud_value = 576000; break;
        # endif
        # ifdef B921600
        case B921600: baud_value = 921600; break;
        # endif
        # ifdef B1000000
        case B1000000: baud_value = 1000000; break;
        # endif
        # ifdef B1152000
        case B1152000: baud_value = 1152000; break;
        # endif
        # ifdef B2000000
        case B2000000: baud_value = 2000000; break;
        # endif
        # ifdef B3000000
        case B3000000: baud_value = 3000000; break;
        # endif
        # ifdef B3500000
        case B3500000: baud_value = 3500000; break;
        # endif
        # ifdef B4000000
        case B4000000: baud_value = 4000000; break;
        # endif
        default:
            dp(DEBUG_ERROR, "cfgetospeed() returned an unhandled "
               "baud rate type <%d> that could not be converted.\n",
               baud);
            goto error;
    }
    
    // If all went well then return the converted baud rate:
    rtn = baud_value;
            
error:
    dp(DEBUG_TRACE, "Exiting get_baud_rate() - returning %d\n", rtn);
    
    return rtn;
}

int get_data_bits(int file_descriptor)
{
    dp(DEBUG_TRACE, "Entering get_data_bits(%d)\n", file_descriptor);
    
    int data_bits = -1;
    
    struct termios term_attrs;
    if (tcgetattr(file_descriptor, &term_attrs) < 0)
    {
        dp(DEBUG_WARN, "tcgetattr() failed to get port settings "
           "for FD <%d> - %s(%d)\n", file_descriptor, strerror(errno), errno);
    }
    else
    {
        if ((term_attrs.c_cflag & CSIZE) == CS5) 
        { 
            data_bits = 5;
        }
        else if ((term_attrs.c_cflag & CSIZE) == CS6)
        { 
            data_bits = 6; 
        }
        else if ((term_attrs.c_cflag & CSIZE) == CS7)
        { 
            data_bits = 7; 
        }
        else if ((term_attrs.c_cflag & CSIZE) == CS8)
        { 
            data_bits = 8; 
        }
        else
        {
            data_bits = 8;
        }
    }
    
    dp(DEBUG_TRACE, "Exiting get_data_bits() - returning %d\n", data_bits);

    return data_bits;
}

int get_stop_bits(int file_descriptor)
{
    dp(DEBUG_TRACE, "Entering get_stop_bits(%d)\n", file_descriptor);
    
    int stop_bits = -1;
    
    struct termios term_attrs;
    if (tcgetattr(file_descriptor, &term_attrs) < 0)
    {
        dp(DEBUG_WARN, "tcgetattr() failed to get port settings "
           "for FD <%d> - %s(%d)\n", file_descriptor, strerror(errno), errno);
    }
    else
    {
        stop_bits = (term_attrs.c_cflag & CSTOPB) ? 2 : 1;
    }
    
    dp(DEBUG_TRACE, "Exiting get_stop_bits() - returning %d\n", stop_bits);
    
    return stop_bits; 
}

int get_parity(int file_descriptor)
{
    dp(DEBUG_TRACE, "Entering get_parity(%d)\n", file_descriptor);
    
    int parity = -1;
    
    struct termios term_attrs;
    if (tcgetattr(file_descriptor, &term_attrs) < 0)
    {
        dp(DEBUG_WARN, "tcgetattr() failed to get port settings "
           "for FD <%d> - %s(%d)\n", file_descriptor, strerror(errno), errno);
    }
    else
    {
        if (term_attrs.c_cflag & PARENB)
        {
            if (term_attrs.c_cflag & PARODD)
            {
                parity = 1;
            }
            else
            {
                parity = 2;
            }
        }
        else
        {
            parity = 0;
        }
    }
    
    dp(DEBUG_TRACE, "Exiting get_parity() - returning %d\n", parity);
    
    return parity;
}

int send_break(int file_descriptor, int duration)
{
    dp(DEBUG_TRACE, "Entering send_break(fd=%d, duration=%d)\n", 
       file_descriptor, duration);
    
    int rtn = -1;
    
    if (tcsendbreak(file_descriptor, duration) < 0)
    {
        dp(DEBUG_WARN, "tcsendbreak() failed for FD <%d> - %s(%d)\n",
           file_descriptor, strerror(errno), errno);
    }
    else
    {
        rtn = 0;
    }
    
    dp(DEBUG_TRACE, "Exiting send_break() - returning %d\n", rtn);
    
    return rtn;
}

int close_port(int file_descriptor)
{
    dp(DEBUG_TRACE, "Entering close_port(%d)\n", file_descriptor);
    
    int rtn = -1;
    
    // Flush non-transmitted and non-read data:
    if (tcflush(file_descriptor, TCIOFLUSH) < 0)
    {
        dp(DEBUG_WARN, "tcflush() failed to flush port "
           "for FD <%d> - %s(%d)\n", file_descriptor, 
           strerror(errno), errno);
    }
    else
    {
        // Try closing the serial port. If interrupted
        // prematurely (EINTR), then try again:
        do 
        {
            rtn = close(file_descriptor);
        } while (rtn < 0 && errno == EINTR);
    }
    
    dp(DEBUG_TRACE, "Exiting close_port() - returning %d\n", rtn);
    
    return rtn;
}

